// Copyright Epic Games, Inc. All Rights Reserved.

#include "config.h"

#include "config/luaconfig.h"
#include "diag/logging.h"

#include <zencore/compactbinarybuilder.h>
#include <zencore/compactbinaryvalidation.h>
#include <zencore/crypto.h>
#include <zencore/except.h>
#include <zencore/fmtutils.h>
#include <zencore/iobuffer.h>
#include <zencore/logging.h>
#include <zencore/string.h>
#include <zenhttp/zenhttp.h>
#include <zenutil/basicfile.h>

ZEN_THIRD_PARTY_INCLUDES_START
#include <fmt/format.h>
#include <zencore/logging.h>
#include <cxxopts.hpp>
#include <sol/sol.hpp>
ZEN_THIRD_PARTY_INCLUDES_END

#if ZEN_PLATFORM_WINDOWS
#	include <conio.h>
#else
#	include <pwd.h>
#	include <unistd.h>
#endif

#include <unordered_map>
#include <unordered_set>

#if ZEN_PLATFORM_WINDOWS

#	include <zencore/windows.h>

// Used for getting My Documents for default data directory
#	include <ShlObj.h>
#	pragma comment(lib, "shell32.lib")
#	pragma comment(lib, "ole32.lib")

namespace zen {

std::filesystem::path
PickDefaultSystemRootDirectory()
{
	// Pick sensible default
	PWSTR	ProgramDataDir = nullptr;
	HRESULT hRes		   = SHGetKnownFolderPath(FOLDERID_ProgramData, 0, NULL, &ProgramDataDir);

	if (SUCCEEDED(hRes))
	{
		std::filesystem::path FinalPath(ProgramDataDir);
		FinalPath /= L"Epic\\Zen";
		::CoTaskMemFree(ProgramDataDir);

		return FinalPath;
	}

	return L"";
}

}  // namespace zen

#else

namespace zen {

std::filesystem::path
PickDefaultSystemRootDirectory()
{
	int			  UserId = getuid();
	const passwd* Passwd = getpwuid(UserId);
	return std::filesystem::path(Passwd->pw_dir) / ".zen";
}

}  // namespace zen

#endif

namespace zen {

std::filesystem::path
PickDefaultStateDirectory(std::filesystem::path SystemRoot)
{
	if (SystemRoot.empty())
		return SystemRoot;

	return SystemRoot / "Data";
}

void
EmitCentralManifest(const std::filesystem::path& SystemRoot, Oid Identifier, CbObject Manifest, std::filesystem::path ManifestPath)
{
	CbObjectWriter Cbo;
	Cbo << "path" << ManifestPath.generic_wstring();
	Cbo << "manifest" << Manifest;

	const std::filesystem::path StatesPath = SystemRoot / "States";

	CreateDirectories(StatesPath);
	WriteFile(StatesPath / fmt::format("{}", Identifier), Cbo.Save().GetBuffer().AsIoBuffer());
}

std::vector<CbObject>
ReadAllCentralManifests(const std::filesystem::path& SystemRoot)
{
	std::vector<CbObject> Manifests;

	DirectoryContent Content;
	GetDirectoryContent(SystemRoot / "States", DirectoryContent::IncludeFilesFlag, Content);

	for (std::filesystem::path& File : Content.Files)
	{
		try
		{
			FileContents	FileData	  = ReadFile(File);
			IoBuffer		DataBuffer	  = FileData.Flatten();
			CbValidateError ValidateError = ValidateCompactBinary(DataBuffer, CbValidateMode::All);

			if (ValidateError == CbValidateError::None)
			{
				Manifests.push_back(LoadCompactBinaryObject(DataBuffer));
			}
			else
			{
				ZEN_WARN("failed to load manifest '{}': {}", File, ToString(ValidateError));
			}
		}
		catch (const std::exception& Ex)
		{
			ZEN_WARN("failed to load manifest '{}': {}", File, Ex.what());
		}
	}

	return Manifests;
}

void
ValidateOptions(ZenServerOptions& ServerOptions)
{
	if (ServerOptions.EncryptionKey.empty() == false)
	{
		const auto Key = zen::AesKey256Bit::FromString(ServerOptions.EncryptionKey);

		if (Key.IsValid() == false)
		{
			throw zen::OptionParseException("Invalid AES encryption key");
		}
	}

	if (ServerOptions.EncryptionIV.empty() == false)
	{
		const auto IV = zen::AesIV128Bit::FromString(ServerOptions.EncryptionIV);

		if (IV.IsValid() == false)
		{
			throw zen::OptionParseException("Invalid AES initialization vector");
		}
	}
	if (ServerOptions.HttpServerConfig.ForceLoopback && ServerOptions.IsDedicated)
	{
		throw zen::OptionParseException("Dedicated server can not be used with forced local server address");
	}
}

UpstreamCachePolicy
ParseUpstreamCachePolicy(std::string_view Options)
{
	if (Options == "readonly")
	{
		return UpstreamCachePolicy::Read;
	}
	else if (Options == "writeonly")
	{
		return UpstreamCachePolicy::Write;
	}
	else if (Options == "disabled")
	{
		return UpstreamCachePolicy::Disabled;
	}
	else
	{
		return UpstreamCachePolicy::ReadWrite;
	}
}

ZenObjectStoreConfig
ParseBucketConfigs(std::span<std::string> Buckets)
{
	using namespace std::literals;

	ZenObjectStoreConfig Cfg;

	// split bucket args in the form of "{BucketName};{LocalPath}"
	for (std::string_view Bucket : Buckets)
	{
		ZenObjectStoreConfig::BucketConfig NewBucket;

		if (auto Idx = Bucket.find_first_of(";"); Idx != std::string_view::npos)
		{
			NewBucket.Name		= Bucket.substr(0, Idx);
			NewBucket.Directory = Bucket.substr(Idx + 1);
		}
		else
		{
			NewBucket.Name = Bucket;
		}

		Cfg.Buckets.push_back(std::move(NewBucket));
	}

	return Cfg;
}

static std::string
MakeSafePath(const std::string_view Path)
{
#if ZEN_PLATFORM_WINDOWS
	if (Path.empty())
	{
		return std::string(Path);
	}

	std::string FixedPath(Path);
	std::replace(FixedPath.begin(), FixedPath.end(), '/', '\\');
	if (!FixedPath.starts_with("\\\\?\\"))
	{
		FixedPath.insert(0, "\\\\?\\");
	}
	return FixedPath;
#else
	return std::string(Path);
#endif
};

class CachePolicyOption : public LuaConfig::OptionValue
{
public:
	CachePolicyOption(UpstreamCachePolicy& Value) : Value(Value) {}
	virtual void Print(std::string_view, zen::StringBuilderBase& StringBuilder) override
	{
		switch (Value)
		{
			case UpstreamCachePolicy::Read:
				StringBuilder.Append("readonly");
				break;
			case UpstreamCachePolicy::Write:
				StringBuilder.Append("writeonly");
				break;
			case UpstreamCachePolicy::Disabled:
				StringBuilder.Append("disabled");
				break;
			case UpstreamCachePolicy::ReadWrite:
				StringBuilder.Append("readwrite");
				break;
			default:
				ZEN_ASSERT(false);
		}
	}
	virtual void Parse(sol::object Object) override
	{
		std::string PolicyString = Object.as<std::string>();
		if (PolicyString == "readonly")
		{
			Value = UpstreamCachePolicy::Read;
		}
		else if (PolicyString == "writeonly")
		{
			Value = UpstreamCachePolicy::Write;
		}
		else if (PolicyString == "disabled")
		{
			Value = UpstreamCachePolicy::Disabled;
		}
		else if (PolicyString == "readwrite")
		{
			Value = UpstreamCachePolicy::ReadWrite;
		}
	}
	UpstreamCachePolicy& Value;
};

class ZenAuthConfigOption : public LuaConfig::OptionValue
{
public:
	ZenAuthConfigOption(ZenAuthConfig& Value) : Value(Value) {}
	virtual void Print(std::string_view Indent, zen::StringBuilderBase& StringBuilder) override
	{
		if (Value.OpenIdProviders.empty())
		{
			StringBuilder.Append("{}");
			return;
		}
		LuaConfig::LuaContainerWriter Writer(StringBuilder, Indent);
		for (const ZenOpenIdProviderConfig& Config : Value.OpenIdProviders)
		{
			Writer.BeginContainer("");
			{
				Writer.WriteValue("name", Config.Name);
				Writer.WriteValue("url", Config.Url);
				Writer.WriteValue("clientid", Config.ClientId);
			}
			Writer.EndContainer();
		}
	}
	virtual void Parse(sol::object Object) override
	{
		if (sol::optional<sol::table> OpenIdProviders = Object.as<sol::table>())
		{
			for (const auto& Kv : OpenIdProviders.value())
			{
				if (sol::optional<sol::table> OpenIdProvider = Kv.second.as<sol::table>())
				{
					std::string Name	 = OpenIdProvider.value().get_or("name", std::string("Default"));
					std::string Url		 = OpenIdProvider.value().get_or("url", std::string());
					std::string ClientId = OpenIdProvider.value().get_or("clientid", std::string());

					Value.OpenIdProviders.push_back({.Name = std::move(Name), .Url = std::move(Url), .ClientId = std::move(ClientId)});
				}
			}
		}
	}
	ZenAuthConfig& Value;
};

class ZenObjectStoreConfigOption : public LuaConfig::OptionValue
{
public:
	ZenObjectStoreConfigOption(ZenObjectStoreConfig& Value) : Value(Value) {}
	virtual void Print(std::string_view Indent, zen::StringBuilderBase& StringBuilder) override
	{
		if (Value.Buckets.empty())
		{
			StringBuilder.Append("{}");
			return;
		}
		LuaConfig::LuaContainerWriter Writer(StringBuilder, Indent);
		for (const ZenObjectStoreConfig::BucketConfig& Config : Value.Buckets)
		{
			Writer.BeginContainer("");
			{
				Writer.WriteValue("name", Config.Name);
				std::string Directory = Config.Directory.string();
				LuaConfig::EscapeBackslash(Directory);
				Writer.WriteValue("directory", Directory);
			}
			Writer.EndContainer();
		}
	}
	virtual void Parse(sol::object Object) override
	{
		if (sol::optional<sol::table> Buckets = Object.as<sol::table>())
		{
			for (const auto& Kv : Buckets.value())
			{
				if (sol::optional<sol::table> Bucket = Kv.second.as<sol::table>())
				{
					std::string Name	  = Bucket.value().get_or("name", std::string("Default"));
					std::string Directory = Bucket.value().get_or("directory", std::string());

					Value.Buckets.push_back({.Name = std::move(Name), .Directory = LuaConfig::MakeSafePath(Directory)});
				}
			}
		}
	}
	ZenObjectStoreConfig& Value;
};

std::shared_ptr<LuaConfig::OptionValue>
MakeOption(zen::UpstreamCachePolicy& Value)
{
	return std::make_shared<CachePolicyOption>(Value);
};

std::shared_ptr<LuaConfig::OptionValue>
MakeOption(zen::ZenAuthConfig& Value)
{
	return std::make_shared<ZenAuthConfigOption>(Value);
};

std::shared_ptr<LuaConfig::OptionValue>
MakeOption(zen::ZenObjectStoreConfig& Value)
{
	return std::make_shared<ZenObjectStoreConfigOption>(Value);
};

void
ParseConfigFile(const std::filesystem::path& Path,
				ZenServerOptions&			 ServerOptions,
				const cxxopts::ParseResult&	 CmdLineResult,
				std::string_view			 OutputConfigFile)
{
	using namespace std::literals;

	LuaConfig::Options LuaOptions;

	////// server
	LuaOptions.AddOption("server.dedicated"sv, ServerOptions.IsDedicated, "dedicated"sv);
	LuaOptions.AddOption("server.logid"sv, ServerOptions.LogId, "log-id"sv);
	LuaOptions.AddOption("server.sentry.disable"sv, ServerOptions.NoSentry, "no-sentry"sv);
	LuaOptions.AddOption("server.sentry.allowpersonalinfo"sv, ServerOptions.SentryAllowPII, "sentry-allow-personal-info"sv);
	LuaOptions.AddOption("server.systemrootdir"sv, ServerOptions.SystemRootDir, "system-dir"sv);
	LuaOptions.AddOption("server.datadir"sv, ServerOptions.DataDir, "data-dir"sv);
	LuaOptions.AddOption("server.contentdir"sv, ServerOptions.ContentDir, "content-dir"sv);
	LuaOptions.AddOption("server.abslog"sv, ServerOptions.AbsLogFile, "abslog"sv);
	LuaOptions.AddOption("server.debug"sv, ServerOptions.IsDebug, "debug"sv);
	LuaOptions.AddOption("server.clean"sv, ServerOptions.IsCleanStart, "clean"sv);
	LuaOptions.AddOption("server.noconsole"sv, ServerOptions.NoConsoleOutput, "quiet"sv);

	////// objectstore
	LuaOptions.AddOption("server.objectstore.enabled"sv, ServerOptions.ObjectStoreEnabled, "objectstore-enabled"sv);
	LuaOptions.AddOption("server.objectstore.buckets"sv, ServerOptions.ObjectStoreConfig);

	////// network
	LuaOptions.AddOption("network.httpserverclass"sv, ServerOptions.HttpServerConfig.ServerClass, "http"sv);
	LuaOptions.AddOption("network.httpserverthreads"sv, ServerOptions.HttpServerConfig.ThreadCount, "http-threads"sv);
	LuaOptions.AddOption("network.port"sv, ServerOptions.BasePort, "port"sv);
	LuaOptions.AddOption("network.forceloopback"sv, ServerOptions.HttpServerConfig.ForceLoopback, "http-forceloopback"sv);

	LuaOptions.AddOption("network.httpsys.async.workthreads"sv,
						 ServerOptions.HttpServerConfig.HttpSys.AsyncWorkThreadCount,
						 "httpsys-async-work-threads"sv);
	LuaOptions.AddOption("network.httpsys.async.response"sv,
						 ServerOptions.HttpServerConfig.HttpSys.IsAsyncResponseEnabled,
						 "httpsys-enable-async-response"sv);
	LuaOptions.AddOption("network.httpsys.requestlogging"sv,
						 ServerOptions.HttpServerConfig.HttpSys.IsRequestLoggingEnabled,
						 "httpsys-enable-request-logging"sv);

#if ZEN_WITH_TRACE
	////// trace
	LuaOptions.AddOption("trace.host"sv, ServerOptions.TraceHost, "tracehost"sv);
	LuaOptions.AddOption("trace.file"sv, ServerOptions.TraceFile, "tracefile"sv);
#endif

	////// stats
	LuaOptions.AddOption("stats.enable"sv, ServerOptions.StatsConfig.Enabled);
	LuaOptions.AddOption("stats.host"sv, ServerOptions.StatsConfig.StatsdHost);
	LuaOptions.AddOption("stats.port"sv, ServerOptions.StatsConfig.StatsdPort);

	////// cache
	LuaOptions.AddOption("cache.enable"sv, ServerOptions.StructuredCacheConfig.Enabled);
	LuaOptions.AddOption("cache.writelog"sv, ServerOptions.StructuredCacheConfig.WriteLogEnabled, "cache-write-log");
	LuaOptions.AddOption("cache.accesslog"sv, ServerOptions.StructuredCacheConfig.AccessLogEnabled, "cache-access-log");

	LuaOptions.AddOption("cache.memlayer.sizethreshold"sv,
						 ServerOptions.StructuredCacheConfig.MemCacheSizeThreshold,
						 "cache-memlayer-sizethreshold");
	LuaOptions.AddOption("cache.memlayer.targetfootprint"sv,
						 ServerOptions.StructuredCacheConfig.MemTargetFootprintBytes,
						 "cache-memlayer-targetfootprint");
	LuaOptions.AddOption("cache.memlayer.triminterval"sv,
						 ServerOptions.StructuredCacheConfig.MemTrimIntervalSeconds,
						 "cache-memlayer-triminterval");
	LuaOptions.AddOption("cache.memlayer.maxage"sv, ServerOptions.StructuredCacheConfig.MemMaxAgeSeconds, "cache-memlayer-maxage");

	////// cache.upstream
	LuaOptions.AddOption("cache.upstream.policy"sv, ServerOptions.UpstreamCacheConfig.CachePolicy, "upstream-cache-policy"sv);
	LuaOptions.AddOption("cache.upstream.upstreamthreadcount"sv,
						 ServerOptions.UpstreamCacheConfig.UpstreamThreadCount,
						 "upstream-thread-count"sv);
	LuaOptions.AddOption("cache.upstream.connecttimeoutms"sv,
						 ServerOptions.UpstreamCacheConfig.ConnectTimeoutMilliseconds,
						 "upstream-connect-timeout-ms"sv);
	LuaOptions.AddOption("cache.upstream.timeoutms"sv, ServerOptions.UpstreamCacheConfig.TimeoutMilliseconds, "upstream-timeout-ms"sv);

	////// cache.upstream.jupiter
	LuaOptions.AddOption("cache.upstream.jupiter.name"sv, ServerOptions.UpstreamCacheConfig.JupiterConfig.Name);
	LuaOptions.AddOption("cache.upstream.jupiter.url"sv, ServerOptions.UpstreamCacheConfig.JupiterConfig.Url, "upstream-jupiter-url"sv);
	LuaOptions.AddOption("cache.upstream.jupiter.oauthprovider"sv,
						 ServerOptions.UpstreamCacheConfig.JupiterConfig.OAuthUrl,
						 "upstream-jupiter-oauth-url"sv);
	LuaOptions.AddOption("cache.upstream.jupiter.oauthclientid"sv,
						 ServerOptions.UpstreamCacheConfig.JupiterConfig.OAuthClientId,
						 "upstream-jupiter-oauth-clientid");
	LuaOptions.AddOption("cache.upstream.jupiter.oauthclientsecret"sv,
						 ServerOptions.UpstreamCacheConfig.JupiterConfig.OAuthClientSecret,
						 "upstream-jupiter-oauth-clientsecret"sv);
	LuaOptions.AddOption("cache.upstream.jupiter.openidprovider"sv,
						 ServerOptions.UpstreamCacheConfig.JupiterConfig.OpenIdProvider,
						 "upstream-jupiter-openid-provider"sv);
	LuaOptions.AddOption("cache.upstream.jupiter.token"sv,
						 ServerOptions.UpstreamCacheConfig.JupiterConfig.AccessToken,
						 "upstream-jupiter-token"sv);
	LuaOptions.AddOption("cache.upstream.jupiter.namespace"sv,
						 ServerOptions.UpstreamCacheConfig.JupiterConfig.Namespace,
						 "upstream-jupiter-namespace"sv);
	LuaOptions.AddOption("cache.upstream.jupiter.ddcnamespace"sv,
						 ServerOptions.UpstreamCacheConfig.JupiterConfig.DdcNamespace,
						 "upstream-jupiter-namespace-ddc"sv);

	////// cache.upstream.zen
	//	LuaOptions.AddOption("cache.upstream.zen"sv, ServerOptions.UpstreamCacheConfig.ZenConfig);
	LuaOptions.AddOption("cache.upstream.zen.name"sv, ServerOptions.UpstreamCacheConfig.ZenConfig.Name);
	LuaOptions.AddOption("cache.upstream.zen.dns"sv, ServerOptions.UpstreamCacheConfig.ZenConfig.Dns);
	LuaOptions.AddOption("cache.upstream.zen.url"sv, ServerOptions.UpstreamCacheConfig.ZenConfig.Urls);

	LuaOptions.AddOption("gc.enabled"sv, ServerOptions.GcConfig.Enabled, "gc-enabled"sv);
	LuaOptions.AddOption("gc.v2"sv, ServerOptions.GcConfig.UseGCV2, "gc-v2"sv);

	LuaOptions.AddOption("gc.monitorintervalseconds"sv, ServerOptions.GcConfig.MonitorIntervalSeconds, "gc-monitor-interval-seconds"sv);
	LuaOptions.AddOption("gc.intervalseconds"sv, ServerOptions.GcConfig.IntervalSeconds, "gc-interval-seconds"sv);
	LuaOptions.AddOption("gc.collectsmallobjects"sv, ServerOptions.GcConfig.CollectSmallObjects, "gc-small-objects"sv);
	LuaOptions.AddOption("gc.diskreservesize"sv, ServerOptions.GcConfig.DiskReserveSize, "disk-reserve-size"sv);
	LuaOptions.AddOption("gc.disksizesoftlimit"sv, ServerOptions.GcConfig.DiskSizeSoftLimit, "gc-disksize-softlimit"sv);
	LuaOptions.AddOption("gc.lowdiskspacethreshold"sv,
						 ServerOptions.GcConfig.MinimumFreeDiskSpaceToAllowWrites,
						 "gc-low-diskspace-threshold"sv);
	LuaOptions.AddOption("gc.lightweightintervalseconds"sv,
						 ServerOptions.GcConfig.LightweightIntervalSeconds,
						 "gc-lightweight-interval-seconds"sv);
	LuaOptions.AddOption("gc.compactblockthreshold"sv,
						 ServerOptions.GcConfig.CompactBlockUsageThresholdPercent,
						 "gc-compactblock-threshold"sv);
	LuaOptions.AddOption("gc.verbose"sv, ServerOptions.GcConfig.Verbose, "gc-verbose"sv);
	LuaOptions.AddOption("gc.single-threaded"sv, ServerOptions.GcConfig.SingleThreaded, "gc-single-threaded"sv);

	////// gc
	LuaOptions.AddOption("gc.cache.maxdurationseconds"sv, ServerOptions.GcConfig.Cache.MaxDurationSeconds, "gc-cache-duration-seconds"sv);

	////// security
	LuaOptions.AddOption("security.encryptionaeskey"sv, ServerOptions.EncryptionKey, "encryption-aes-key"sv);
	LuaOptions.AddOption("security.encryptionaesiv"sv, ServerOptions.EncryptionIV, "encryption-aes-iv"sv);
	LuaOptions.AddOption("security.openidproviders"sv, ServerOptions.AuthConfig);

	LuaOptions.Parse(Path, CmdLineResult);

	////// workspaces
	LuaOptions.AddOption("workspaces.enabled"sv, ServerOptions.WorksSpacesConfig.Enabled, "workspaces-enabled"sv);

	// These have special command line processing so we make sure we export them if they were configured on command line
	if (!ServerOptions.AuthConfig.OpenIdProviders.empty())
	{
		LuaOptions.Touch("security.openidproviders"sv);
	}
	if (!ServerOptions.ObjectStoreConfig.Buckets.empty())
	{
		LuaOptions.Touch("server.objectstore.buckets"sv);
	}

	if (!OutputConfigFile.empty())
	{
		std::filesystem::path			  WritePath(MakeSafePath(OutputConfigFile));
		zen::ExtendableStringBuilder<512> ConfigStringBuilder;
		LuaOptions.Print(ConfigStringBuilder, CmdLineResult);
		zen::BasicFile Output;
		Output.Open(WritePath, zen::BasicFile::Mode::kTruncate);
		Output.Write(ConfigStringBuilder.Data(), ConfigStringBuilder.Size(), 0);
	}
}

void
ParseCliOptions(int argc, char* argv[], ZenServerOptions& ServerOptions)
{
	const char* DefaultHttp = "asio";

#if ZEN_WITH_HTTPSYS
	if (!zen::windows::IsRunningOnWine())
	{
		DefaultHttp = "httpsys";
	}
#endif

	// Note to those adding future options; std::filesystem::path-type options
	// must be read into a std::string first. As of cxxopts-3.0.0 it uses a >>
	// stream operator to convert argv value into the options type. std::fs::path
	// expects paths in streams to be quoted but argv paths are unquoted. By
	// going into a std::string first, paths with whitespace parse correctly.
	std::string SystemRootDir;
	std::string DataDir;
	std::string ContentDir;
	std::string AbsLogFile;
	std::string ConfigFile;
	std::string OutputConfigFile;
	std::string BaseSnapshotDir;

	cxxopts::Options options("zenserver", "Zen Server");
	options.add_options()("dedicated",
						  "Enable dedicated server mode",
						  cxxopts::value<bool>(ServerOptions.IsDedicated)->default_value("false"));
	options.add_options()("d, debug", "Enable debugging", cxxopts::value<bool>(ServerOptions.IsDebug)->default_value("false"));
	options.add_options()("clean",
						  "Clean out all state at startup",
						  cxxopts::value<bool>(ServerOptions.IsCleanStart)->default_value("false"));
	options.add_options()("scrub",
						  "Validate state at startup",
						  cxxopts::value(ServerOptions.ScrubOptions)->implicit_value("yes"),
						  "(nocas,nogc,nodelete,yes,no)*");
	options.add_options()("help", "Show command line help");
	options.add_options()("t, test", "Enable test mode", cxxopts::value<bool>(ServerOptions.IsTest)->default_value("false"));
	options.add_options()("data-dir", "Specify persistence root", cxxopts::value<std::string>(DataDir));
	options.add_options()("system-dir", "Specify system root", cxxopts::value<std::string>(SystemRootDir));
	options.add_options()("snapshot-dir",
						  "Specify a snapshot of server state to mirror into the persistence root at startup",
						  cxxopts::value<std::string>(BaseSnapshotDir));
	options.add_options()("content-dir", "Frontend content directory", cxxopts::value<std::string>(ContentDir));
	options.add_options()("powercycle",
						  "Exit immediately after initialization is complete",
						  cxxopts::value<bool>(ServerOptions.IsPowerCycle));
	options.add_options()("config", "Path to Lua config file", cxxopts::value<std::string>(ConfigFile));
	options.add_options()("write-config", "Path to output Lua config file", cxxopts::value<std::string>(OutputConfigFile));
	options.add_options()("no-sentry",
						  "Disable Sentry crash handler",
						  cxxopts::value<bool>(ServerOptions.NoSentry)->default_value("false"));
	options.add_options()("sentry-allow-personal-info",
						  "Allow personally identifiable information in sentry crash reports",
						  cxxopts::value<bool>(ServerOptions.SentryAllowPII)->default_value("false"));

	// clang-format off
	options.add_options("logging")
		("abslog",			"Path to log file",								cxxopts::value<std::string>(AbsLogFile))
		("log-id",			"Specify id for adding context to log output",	cxxopts::value<std::string>(ServerOptions.LogId))
		("quiet", 			"Disable console logging", 						cxxopts::value<bool>(ServerOptions.NoConsoleOutput)->default_value("false"))
		("log-trace", 		"Change selected loggers to level TRACE", 		cxxopts::value<std::string>(ServerOptions.Loggers[logging::level::Trace]))
		("log-debug", 		"Change selected loggers to level DEBUG", 		cxxopts::value<std::string>(ServerOptions.Loggers[logging::level::Debug]))
		("log-info", 		"Change selected loggers to level INFO", 		cxxopts::value<std::string>(ServerOptions.Loggers[logging::level::Info]))
		("log-warn", 		"Change selected loggers to level WARN", 		cxxopts::value<std::string>(ServerOptions.Loggers[logging::level::Warn]))
		("log-error", 		"Change selected loggers to level ERROR", 		cxxopts::value<std::string>(ServerOptions.Loggers[logging::level::Err]))
		("log-critical", 	"Change selected loggers to level CRITICAL", 	cxxopts::value<std::string>(ServerOptions.Loggers[logging::level::Critical]))
		("log-off", 		"Change selected loggers to level OFF", 		cxxopts::value<std::string>(ServerOptions.Loggers[logging::level::Off]))
	;
	// clang-format on

	options.add_option("security",
					   "",
					   "encryption-aes-key",
					   "256 bit AES encryption key",
					   cxxopts::value<std::string>(ServerOptions.EncryptionKey),
					   "");

	options.add_option("security",
					   "",
					   "encryption-aes-iv",
					   "128 bit AES encryption initialization vector",
					   cxxopts::value<std::string>(ServerOptions.EncryptionIV),
					   "");

	std::string OpenIdProviderName;
	options.add_option("security",
					   "",
					   "openid-provider-name",
					   "Open ID provider name",
					   cxxopts::value<std::string>(OpenIdProviderName),
					   "Default");

	std::string OpenIdProviderUrl;
	options.add_option("security", "", "openid-provider-url", "Open ID provider URL", cxxopts::value<std::string>(OpenIdProviderUrl), "");

	std::string OpenIdClientId;
	options.add_option("security", "", "openid-client-id", "Open ID client ID", cxxopts::value<std::string>(OpenIdClientId), "");

	options
		.add_option("lifetime", "", "owner-pid", "Specify owning process id", cxxopts::value<int>(ServerOptions.OwnerPid), "<identifier>");
	options.add_option("lifetime",
					   "",
					   "child-id",
					   "Specify id which can be used to signal parent",
					   cxxopts::value<std::string>(ServerOptions.ChildId),
					   "<identifier>");

#if ZEN_PLATFORM_WINDOWS
	options.add_option("lifetime",
					   "",
					   "install",
					   "Install zenserver as a Windows service",
					   cxxopts::value<bool>(ServerOptions.InstallService),
					   "");
	options.add_option("lifetime",
					   "",
					   "uninstall",
					   "Uninstall zenserver as a Windows service",
					   cxxopts::value<bool>(ServerOptions.UninstallService),
					   "");
#endif

	options.add_option("network",
					   "",
					   "http",
					   "Select HTTP server implementation (asio|httpsys|null)",
					   cxxopts::value<std::string>(ServerOptions.HttpServerConfig.ServerClass)->default_value(DefaultHttp),
					   "<http class>");

	options.add_option("network",
					   "",
					   "http-threads",
					   "Number of http server connection threads",
					   cxxopts::value<unsigned int>(ServerOptions.HttpServerConfig.ThreadCount),
					   "<http threads>");

	options.add_option("network",
					   "p",
					   "port",
					   "Select HTTP port",
					   cxxopts::value<int>(ServerOptions.BasePort)->default_value("8558"),
					   "<port number>");

	options.add_option("network",
					   "",
					   "http-forceloopback",
					   "Force using local loopback interface",
					   cxxopts::value<bool>(ServerOptions.HttpServerConfig.ForceLoopback)->default_value("false"),
					   "<http forceloopback>");

	options.add_option("httpsys",
					   "",
					   "httpsys-async-work-threads",
					   "Number of HttpSys async worker threads",
					   cxxopts::value<unsigned int>(ServerOptions.HttpServerConfig.HttpSys.AsyncWorkThreadCount),
					   "<httpsys workthreads>");

	options.add_option("httpsys",
					   "",
					   "httpsys-enable-async-response",
					   "Enables Httpsys async response",
					   cxxopts::value<bool>(ServerOptions.HttpServerConfig.HttpSys.IsAsyncResponseEnabled)->default_value("true"),
					   "<httpsys async response>");

	options.add_option("httpsys",
					   "",
					   "httpsys-enable-request-logging",
					   "Enables Httpsys request logging",
					   cxxopts::value<bool>(ServerOptions.HttpServerConfig.HttpSys.IsRequestLoggingEnabled),
					   "<httpsys request logging>");

#if ZEN_WITH_TRACE
	options.add_option("ue-trace",
					   "",
					   "tracehost",
					   "Hostname to send the trace to",
					   cxxopts::value<std::string>(ServerOptions.TraceHost)->default_value(""),
					   "");

	options.add_option("ue-trace",
					   "",
					   "tracefile",
					   "Path to write a trace to",
					   cxxopts::value<std::string>(ServerOptions.TraceFile)->default_value(""),
					   "");
#endif	// ZEN_WITH_TRACE

	options.add_option("diagnostics",
					   "",
					   "crash",
					   "Simulate a crash",
					   cxxopts::value<bool>(ServerOptions.ShouldCrash)->default_value("false"),
					   "");

	std::string UpstreamCachePolicyOptions;
	options.add_option("cache",
					   "",
					   "upstream-cache-policy",
					   "",
					   cxxopts::value<std::string>(UpstreamCachePolicyOptions)->default_value(""),
					   "Upstream cache policy (readwrite|readonly|writeonly|disabled)");

	options.add_option("cache",
					   "",
					   "upstream-jupiter-url",
					   "URL to a Jupiter instance",
					   cxxopts::value<std::string>(ServerOptions.UpstreamCacheConfig.JupiterConfig.Url)->default_value(""),
					   "");

	options.add_option("cache",
					   "",
					   "upstream-jupiter-oauth-url",
					   "URL to the OAuth provier",
					   cxxopts::value<std::string>(ServerOptions.UpstreamCacheConfig.JupiterConfig.OAuthUrl)->default_value(""),
					   "");

	options.add_option("cache",
					   "",
					   "upstream-jupiter-oauth-clientid",
					   "The OAuth client ID",
					   cxxopts::value<std::string>(ServerOptions.UpstreamCacheConfig.JupiterConfig.OAuthClientId)->default_value(""),
					   "");

	options.add_option("cache",
					   "",
					   "upstream-jupiter-oauth-clientsecret",
					   "The OAuth client secret",
					   cxxopts::value<std::string>(ServerOptions.UpstreamCacheConfig.JupiterConfig.OAuthClientSecret)->default_value(""),
					   "");

	options.add_option("cache",
					   "",
					   "upstream-jupiter-openid-provider",
					   "Name of a registered Open ID provider",
					   cxxopts::value<std::string>(ServerOptions.UpstreamCacheConfig.JupiterConfig.OpenIdProvider)->default_value(""),
					   "");

	options.add_option("cache",
					   "",
					   "upstream-jupiter-token",
					   "A static authentication token",
					   cxxopts::value<std::string>(ServerOptions.UpstreamCacheConfig.JupiterConfig.AccessToken)->default_value(""),
					   "");

	options.add_option("cache",
					   "",
					   "upstream-jupiter-namespace",
					   "The Common Blob Store API namespace",
					   cxxopts::value<std::string>(ServerOptions.UpstreamCacheConfig.JupiterConfig.Namespace)->default_value(""),
					   "");

	options.add_option("cache",
					   "",
					   "upstream-jupiter-namespace-ddc",
					   "The lecacy DDC namespace",
					   cxxopts::value<std::string>(ServerOptions.UpstreamCacheConfig.JupiterConfig.DdcNamespace)->default_value(""),
					   "");

	options.add_option("cache",
					   "",
					   "upstream-zen-url",
					   "URL to remote Zen server. Use a comma separated list to choose the one with the best latency.",
					   cxxopts::value<std::vector<std::string>>(ServerOptions.UpstreamCacheConfig.ZenConfig.Urls),
					   "");

	options.add_option("cache",
					   "",
					   "upstream-zen-dns",
					   "DNS that resolves to one or more Zen server instance(s)",
					   cxxopts::value<std::vector<std::string>>(ServerOptions.UpstreamCacheConfig.ZenConfig.Dns),
					   "");

	options.add_option("cache",
					   "",
					   "upstream-thread-count",
					   "Number of threads used for upstream procsssing",
					   cxxopts::value<int32_t>(ServerOptions.UpstreamCacheConfig.UpstreamThreadCount)->default_value("4"),
					   "");

	options.add_option("cache",
					   "",
					   "upstream-connect-timeout-ms",
					   "Connect timeout in millisecond(s). Default 5000 ms.",
					   cxxopts::value<int32_t>(ServerOptions.UpstreamCacheConfig.ConnectTimeoutMilliseconds)->default_value("5000"),
					   "");

	options.add_option("cache",
					   "",
					   "upstream-timeout-ms",
					   "Timeout in millisecond(s). Default 0 ms",
					   cxxopts::value<int32_t>(ServerOptions.UpstreamCacheConfig.TimeoutMilliseconds)->default_value("0"),
					   "");

	options.add_option("cache",
					   "",
					   "cache-write-log",
					   "Whether cache write log is enabled",
					   cxxopts::value<bool>(ServerOptions.StructuredCacheConfig.WriteLogEnabled)->default_value("false"),
					   "");

	options.add_option("cache",
					   "",
					   "cache-access-log",
					   "Whether cache access log is enabled",
					   cxxopts::value<bool>(ServerOptions.StructuredCacheConfig.AccessLogEnabled)->default_value("false"),
					   "");

	options.add_option(
		"cache",
		"",
		"cache-memlayer-sizethreshold",
		"The largest size of a cache entry that may be cached in memory. Default set to 1024 (1 Kb). Set to 0 to disable memory caching.",
		cxxopts::value<uint64_t>(ServerOptions.StructuredCacheConfig.MemCacheSizeThreshold)->default_value("1024"),
		"");

	options.add_option("cache",
					   "",
					   "cache-memlayer-targetfootprint",
					   "Max allowed memory used by cache memory layer per namespace in bytes. Default set to 536870912 (512 Mb).",
					   cxxopts::value<uint64_t>(ServerOptions.StructuredCacheConfig.MemTargetFootprintBytes)->default_value("536870912"),
					   "");

	options.add_option("cache",
					   "",
					   "cache-memlayer-triminterval",
					   "Minimum time between each attempt to trim cache memory layers in seconds. Default set to 60 (1 min).",
					   cxxopts::value<uint64_t>(ServerOptions.StructuredCacheConfig.MemTrimIntervalSeconds)->default_value("60"),
					   "");

	options.add_option("cache",
					   "",
					   "cache-memlayer-maxage",
					   "Maximum age of payloads when trimming cache memory layers in seconds. Default set to 86400 (1 day).",
					   cxxopts::value<uint64_t>(ServerOptions.StructuredCacheConfig.MemMaxAgeSeconds)->default_value("86400"),
					   "");

	options.add_option("gc",
					   "",
					   "gc-enabled",
					   "Whether garbage collection is enabled or not.",
					   cxxopts::value<bool>(ServerOptions.GcConfig.Enabled)->default_value("true"),
					   "");

	options.add_option("gc",
					   "",
					   "gc-v2",
					   "Use V2 of GC implementation or not.",
					   cxxopts::value<bool>(ServerOptions.GcConfig.UseGCV2)->default_value("true"),
					   "");

	options.add_option("gc",
					   "",
					   "gc-small-objects",
					   "Whether garbage collection of small objects is enabled or not.",
					   cxxopts::value<bool>(ServerOptions.GcConfig.CollectSmallObjects)->default_value("true"),
					   "");

	options.add_option("gc",
					   "",
					   "gc-interval-seconds",
					   "Garbage collection interval in seconds. Default set to 3600 (1 hour).",
					   cxxopts::value<int32_t>(ServerOptions.GcConfig.IntervalSeconds)->default_value("3600"),
					   "");

	options.add_option("gc",
					   "",
					   "gc-lightweight-interval-seconds",
					   "Lightweight garbage collection interval in seconds. Default set to 900 (30 min).",
					   cxxopts::value<int32_t>(ServerOptions.GcConfig.LightweightIntervalSeconds)->default_value("900"),
					   "");

	options.add_option("gc",
					   "",
					   "gc-cache-duration-seconds",
					   "Max duration in seconds before Z$ entries get evicted. Default set to 1209600 (2 weeks)",
					   cxxopts::value<int32_t>(ServerOptions.GcConfig.Cache.MaxDurationSeconds)->default_value("1209600"),
					   "");

	options.add_option("gc",
					   "",
					   "gc-projectstore-duration-seconds",
					   "Max duration in seconds before project store entries get evicted. Default set to 1209600 (2 weeks)",
					   cxxopts::value<int32_t>(ServerOptions.GcConfig.ProjectStore.MaxDurationSeconds)->default_value("1209600"),
					   "");

	options.add_option("gc",
					   "",
					   "disk-reserve-size",
					   "Size of gc disk reserve in bytes. Default set to 268435456 (256 Mb). Set to zero to disable.",
					   cxxopts::value<uint64_t>(ServerOptions.GcConfig.DiskReserveSize)->default_value("268435456"),
					   "");

	options.add_option("gc",
					   "",
					   "gc-monitor-interval-seconds",
					   "Garbage collection monitoring interval in seconds. Default set to 30 (30 seconds)",
					   cxxopts::value<int32_t>(ServerOptions.GcConfig.MonitorIntervalSeconds)->default_value("30"),
					   "");

	options.add_option("gc",
					   "",
					   "gc-low-diskspace-threshold",
					   "Minimum free space on disk to allow writes to disk. Default set to 268435456 (256 Mb). Set to zero to disable.",
					   cxxopts::value<uint64_t>(ServerOptions.GcConfig.MinimumFreeDiskSpaceToAllowWrites)->default_value("268435456"),
					   "");

	options.add_option("gc",
					   "",
					   "gc-disksize-softlimit",
					   "Garbage collection disk usage soft limit. Default set to 0 (Off).",
					   cxxopts::value<uint64_t>(ServerOptions.GcConfig.DiskSizeSoftLimit)->default_value("0"),
					   "");

	options.add_option("gc",
					   "",
					   "gc-compactblock-threshold",
					   "Garbage collection - how much of a compact block should be used to skip compacting the block. 0 - compact only "
					   "empty eligible blocks, 100 - compact all non-full eligible blocks.",
					   cxxopts::value<uint32_t>(ServerOptions.GcConfig.CompactBlockUsageThresholdPercent)->default_value("60"),
					   "");

	options.add_option("gc",
					   "",
					   "gc-verbose",
					   "Enable verbose logging for GC.",
					   cxxopts::value<bool>(ServerOptions.GcConfig.Verbose)->default_value("false"),
					   "");

	options.add_option("gc",
					   "",
					   "gc-single-threaded",
					   "Force GC to run single threaded.",
					   cxxopts::value<bool>(ServerOptions.GcConfig.SingleThreaded)->default_value("false"),
					   "");

	options.add_option("objectstore",
					   "",
					   "objectstore-enabled",
					   "Whether the object store is enabled or not.",
					   cxxopts::value<bool>(ServerOptions.ObjectStoreEnabled)->default_value("false"),
					   "");

	std::vector<std::string> BucketConfigs;
	options.add_option("objectstore",
					   "",
					   "objectstore-bucket",
					   "Object store bucket mappings.",
					   cxxopts::value<std::vector<std::string>>(BucketConfigs),
					   "");

	options.add_option("stats",
					   "",
					   "statsd",
					   "",
					   cxxopts::value<bool>(ServerOptions.StatsConfig.Enabled)->default_value("false"),
					   "Enable statsd reporter (localhost:8125)");

	options.add_option("stats",
					   "",
					   "workspaces-enabled",
					   "",
					   cxxopts::value<bool>(ServerOptions.WorksSpacesConfig.Enabled)->default_value("false"),
					   "Enable workspaces support with folder sharing");

	try
	{
		cxxopts::ParseResult Result;

		try
		{
			Result = options.parse(argc, argv);
		}
		catch (const std::exception& Ex)
		{
			throw zen::OptionParseException(Ex.what());
		}

		if (Result.count("help"))
		{
			ZEN_CONSOLE("{}", options.help());
#if ZEN_PLATFORM_WINDOWS
			ZEN_CONSOLE("Press any key to exit!");
			_getch();
#else
			// Assume the user's in a terminal on all other platforms and that
			// they'll use less/more/etc. if need be.
#endif
			exit(0);
		}

		for (int i = 0; i < logging::level::LogLevelCount; ++i)
		{
			logging::ConfigureLogLevels(logging::level::LogLevel(i), ServerOptions.Loggers[i]);
		}
		logging::RefreshLogLevels();

		ServerOptions.SystemRootDir					  = MakeSafePath(SystemRootDir);
		ServerOptions.DataDir						  = MakeSafePath(DataDir);
		ServerOptions.BaseSnapshotDir				  = MakeSafePath(BaseSnapshotDir);
		ServerOptions.ContentDir					  = MakeSafePath(ContentDir);
		ServerOptions.AbsLogFile					  = MakeSafePath(AbsLogFile);
		ServerOptions.ConfigFile					  = MakeSafePath(ConfigFile);
		ServerOptions.UpstreamCacheConfig.CachePolicy = ParseUpstreamCachePolicy(UpstreamCachePolicyOptions);

		if (!BaseSnapshotDir.empty())
		{
			if (DataDir.empty())
				throw zen::OptionParseException("You must explicitly specify a data directory when specifying a base snapshot");

			if (!std::filesystem::is_directory(ServerOptions.BaseSnapshotDir))
				throw OptionParseException(fmt::format("Snapshot directory must be a directory: '{}", BaseSnapshotDir));
		}

		if (OpenIdProviderUrl.empty() == false)
		{
			if (OpenIdClientId.empty())
			{
				throw zen::OptionParseException("Invalid OpenID client ID");
			}

			ServerOptions.AuthConfig.OpenIdProviders.push_back(
				{.Name = OpenIdProviderName, .Url = OpenIdProviderUrl, .ClientId = OpenIdClientId});
		}

		ServerOptions.ObjectStoreConfig = ParseBucketConfigs(BucketConfigs);

		if (!ServerOptions.ConfigFile.empty())
		{
			ParseConfigFile(ServerOptions.ConfigFile, ServerOptions, Result, OutputConfigFile);
		}
		else
		{
			ParseConfigFile(ServerOptions.DataDir / "zen_cfg.lua", ServerOptions, Result, OutputConfigFile);
		}

		ValidateOptions(ServerOptions);
	}
	catch (const zen::OptionParseException& e)
	{
		ZEN_CONSOLE_ERROR("Error parsing zenserver arguments: {}\n\n{}", e.what(), options.help());

		throw;
	}

	if (ServerOptions.SystemRootDir.empty())
	{
		ServerOptions.SystemRootDir = PickDefaultSystemRootDirectory();
	}

	if (ServerOptions.DataDir.empty())
	{
		ServerOptions.DataDir = PickDefaultStateDirectory(ServerOptions.SystemRootDir);
	}

	if (ServerOptions.AbsLogFile.empty())
	{
		ServerOptions.AbsLogFile = ServerOptions.DataDir / "logs" / "zenserver.log";
	}

	ServerOptions.HttpServerConfig.IsDedicatedServer = ServerOptions.IsDedicated;
}

}  // namespace zen
